package org.xx.armory.spring5.mvc;

import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Pageable;
import org.springframework.validation.BindingResult;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
import org.xx.armory.commons.ForLogging;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.apache.commons.text.StringEscapeUtils.escapeJava;
import static org.xx.armory.commons.Converter.toStr;
import static org.xx.armory.commons.TextUtils.ellipse;
import static org.xx.armory.spring5.mvc.ModelUtils.JSON_VIEW;
import static org.xx.armory.spring5.mvc.ModelUtils.SINGLE_BEAN_NAME;
import static org.xx.armory.spring5.mvc.WebUtils.getFullPath;

/**
 * 用于在日志中记录访问地址和映射结果的拦截器。
 */
public class LogInterceptor implements HandlerInterceptor {
    private static final String FULL_PATH_ATTRIBUTE = LogInterceptor.class.getName() + ".fullPath";
    private static final String ISO8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ssX";
    @ForLogging
    private final Logger logger = LoggerFactory.getLogger(LogInterceptor.class);

    @Value("${armory.mvc.log.enabled:true}")
    private boolean enabled;

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler
    )
            throws Exception {
        if (!this.enabled) {
            return true;
        }

        final var fullPath = getFullPath(request);
        if (fullPath.equals("/error") || fullPath.startsWith("/error/")) {
            return true;
        }

        request.setAttribute(FULL_PATH_ATTRIBUTE, fullPath);

        logger.debug("{} \"{}\" map to {}", request.getMethod(), fullPath, handler);

        return true;
    }

    @Override
    public void postHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler,
            ModelAndView modelAndView
    )
            throws Exception {
        if (!this.enabled) {
            return;
        }

        if (modelAndView == null) {
            return;
        }

        final var view = modelAndView.getView();
        final var viewName = modelAndView.getViewName();
        if (view == null && viewName == null) {
            return;
        }

        final var fullPath = toStr(request.getAttribute(FULL_PATH_ATTRIBUTE));
        if (fullPath.isEmpty() || fullPath.equals("/error") || fullPath.startsWith("/error/")) {
            return;
        }

        if (view != null) {
            if (view instanceof RedirectView) {
                logger.debug("{} \"{}\" -> redirect to \"{}\"", request.getMethod(), fullPath,
                             ((RedirectView) view).getUrl());
            } else {
                logger.debug("{} \"{}\" -> {}", request.getMethod(), fullPath, view);
            }
            return;
        }

        final var modelMap = modelAndView.getModelMap();
        final String modelStr;
        final var jsonMap = new HashMap<String, Object>(modelMap.size());
        modelMap.forEach((key, value) -> {
            if (!(value instanceof BindingResult) && !key.equals(JsonView.class.getName())
                    && !key.equals(FilterProvider.class.getName())) {
                jsonMap.put(key, value);
            }
        });

        if (jsonMap.isEmpty()) {
            modelStr = "{}";
        } else if (jsonMap.size() == 1 && jsonMap.containsKey(SINGLE_BEAN_NAME)) {
            final var singleBean = jsonMap.values().iterator().next();
            modelStr = singleBean != null ? dumpJson(singleBean) : "";
        } else {
            modelStr = dumpJson(jsonMap);
        }

        if (viewName.equals(JSON_VIEW)) {
            logger.debug("{} \"{}\" -> json {}", request.getMethod(), fullPath, modelStr);
        } else {
            logger.debug("{} \"{}\" -> template/html({}) {}", request.getMethod(), fullPath, viewName, modelStr);
        }
    }

    private String dumpJson(Map<? extends CharSequence, ?> map) {
        if (map == null) {
            return "null";
        }

        final var result = new StringBuilder();

        result.append("{");
        int c = 0;
        for (final var entry : map.entrySet()) {
            if (c > 0) {
                result.append(",");
            }
            final var key = entry.getKey();
            final var value = entry.getValue();
            if (key == null || key.length() == 0) {
                continue;
            }
            if (value == null) {
                continue;
            }
            result.append(key).append(":").append(dumpJson(value));
            ++c;
        }
        result.append("}");

        return result.toString();
    }

    private String dumpJson(Object obj) {
        if (obj instanceof CharSequence) {
            return "\"" + escapeJava(ellipse((CharSequence) obj, 16)) + "\"";
        } else if (obj instanceof Number) {
            return obj.toString();
        } else if (obj instanceof Boolean) {
            return obj.toString();
        } else if (obj instanceof Character) {
            return "'" + escapeJava(obj.toString()) + "'";
        } else if (obj instanceof Date) {
            return "/" + DateFormatUtils.format((Date) obj, ISO8601_FORMAT) + "/";
        } else if (obj instanceof java.util.List) {
            return "list[size=" + ((List<?>) obj).size() + "]";
        } else if (obj instanceof java.util.Set) {
            return "set[size=" + ((Set<?>) obj).size() + "]";
        } else if (obj instanceof java.util.Collection) {
            return "collection[size=" + ((Collection<?>) obj).size() + "]";
        } else if (obj instanceof java.util.Map) {
            return "map[size=" + ((Map<?, ?>) obj).size() + "]";
        } else if (obj instanceof Pageable) {
            return obj.toString();
        } else {
            return obj.getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(obj));
        }
    }
}
